/*
 * Orthographic projection. Adapted from:
 *    Map Projections: A Working Manual, Snyder, John P: pubs.er.usgs.gov/publication/pp1395
 *    See page 145.
 */

import {DEG, RAD} from "../util/consts.js";
import {floorMod} from "../util/math.js";
import ORTHOGRAPHIC_FRAG from "./orthographic.frag";


/**
 * Decomposes a D3 projection's configuration into arguments for the projection.
 * @param proj a D3 projection
 * @returns {number[]} the args array for {@link orthographicProjection}.
 */
export function orthographicArgsFromD3(proj) {
    // TODO: add support for γ rotation, angle, center, clipExtent, clipAngle...
    const [λ0, φ0] = proj.rotate();
    const [x0, y0] = proj.translate();
    return [proj.scale(), -λ0, -φ0, x0, y0];
}

/**
 * @param {number} R radius of the sphere (i.e., scale)
 * @param {number} λ0 longitude of projection center (degrees)
 * @param {number} φ0 latitude of projection center (degrees)
 * @param {number} x0 translation along x axis
 * @param {number} y0 translation along y axis
 * @returns {{project: function, invert: function, webgl: function}}
 */
export function orthographicProjection(R, λ0, φ0, x0, y0) {

    // Check if φ0 is rotated far enough that the globe is upside down. If so, adjust the projection center and
    // flip the x,y space. For example, rotation of +100 is actually lat of 80 deg with lon on other side.

    const φnorm = floorMod(φ0 + 90, 360);  // now on range [0, 360). Anything on range (180, 360) is flipped.
    const flip = 180 < φnorm ? -1 : 1;
    if (flip < 0) {
        φ0 = 270 - φnorm;
        λ0 += 180;
    } else {
        φ0 = φnorm - 90;
    }
    φ0 *= RAD;
    λ0 = (floorMod(λ0 + 180, 360) - 180)*RAD;  // normalize to [-180, 180)

    const R2 = R*R;
    const sinφ0 = Math.sin(φ0);
    const cosφ0 = Math.cos(φ0);
    const Rcosφ0 = R * cosφ0;
    const cosφ0dR = cosφ0 / R;
    const center = [x0, y0];

    /**
     * @param {number} lon latitude (degrees)
     * @param {number} lat longitude (degrees)
     * @returns {number[]} resulting [x, y] or [NaN, NaN] if the coordinates are not defined for the projection.
     */
    function project(lon, lat) {
        const λ = lon * RAD;
        const φ = lat * RAD;
        const Δλ = λ - λ0;
        const sinΔλ = Math.sin(Δλ);
        const cosΔλ = Math.cos(Δλ);
        const sinφ = Math.sin(φ);
        const cosφ = Math.cos(φ);
        const Rcosφ = R * cosφ;
        // const cosc = sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ;  // test if clip angle > 90°
        // if (cosc < 0) return [NaN, NaN];
        const px = Rcosφ * sinΔλ;
        const py = Rcosφ * cosΔλ * sinφ0 - Rcosφ0 * sinφ;  // negates y because it grows downward
        const x = px * flip + x0;
        const y = py * flip + y0;
        return [x, y];
    }

    /**
     * @param {number} x screen x coordinate
     * @param {number} y screen y coordinate
     * @returns {number[]} resulting [λ, φ] in degrees or [NaN, NaN] if the point is not defined for the projection.
     */
    function invert(x, y) {
        const px = (x - x0) * flip;
        const py = (y0 - y) * flip;  // negate y because it grows downward

        // const ρ = Math.sqrt(x * x + y * y);   // positive number
        // const c = Math.asin(ρ / R);           // [0, π/2] or NaN when ρ > R (meaning the point is outside the globe)
        // const sinc = Math.sin(c);             // [0, 1] because c in range [0, π/2]
        // const cosc = Math.cos(c);             // [0, 1] because c in range [0, π/2]
        // const ysinc = y * sinc;
        // const λ = λ0 + Math.atan2(x * sinc, ρ * cosc * cosφ0 - ysinc * sinφ0);
        // const φ = ρ === 0 ? φ0 : Math.asin(cosc * sinφ0 + ysinc * cosφ0 / ρ);

        const ρ2 = px * px + py * py;
        const d = 1 - ρ2 / R2;
        if (d >= 0) {
            const cosc = Math.sqrt(d);  // cos(asin(a)) == sqrt(1 - a*a)
            const λ = λ0 + Math.atan2(px, cosc * Rcosφ0 - py * sinφ0);
            const φ = Math.asin(cosc * sinφ0 + py * cosφ0dR);
            return [λ * DEG, φ * DEG];
        }
        return [NaN, NaN];  // outside of projection
    }

    function webgl() {
        return {
            shaderSource: ORTHOGRAPHIC_FRAG,
            uniforms: {
                u_translate: center,   // screen coords translation (x0, y0)
                u_R2: R2,              // scale R, squared
                u_lon0: λ0,            // origin longitude
                u_sinlat0: sinφ0,      // sin(lat0)
                u_Rcoslat0: Rcosφ0,    // R * cos(lat0)
                u_coslat0dR: cosφ0dR,  // cos(lat0) / R
                u_flip: flip,          // 1.0 if lat0 in range [-90deg, +90deg], otherwise -1.0
            },
        };
    }

    return {project, invert, webgl};
}
